package net.sf.midollo.net.httptunnel.client;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import net.sf.midollo.io.IOStream;
import net.sf.midollo.io.IOStreamFactory;
import net.sf.midollo.io.text.Formatter;
import net.sf.midollo.io.text.Parser;
import net.sf.midollo.net.http.text.HttpFormatter;
import net.sf.midollo.net.http.text.HttpParser;
import net.sf.midollo.net.httptunnel.HttpTunnelConstants;
import net.sf.midollo.utils.Utils;

public class HttpTunnelClient implements HttpTunnelConstants {

  protected final ByteArrayOutputStream writerHeadBuffer;
  protected final ByteArrayOutputStream writerTailBuffer;
  protected final String uri;
  protected final IOStreamFactory ioStreamFactory;
  protected IOStream ioRequest;
  protected IOStream ioResponse;
  protected InputStream inRequest;
  protected InputStream inResponse;
  protected OutputStream outRequest;
  protected OutputStream outResponse;
  protected String sessionId;
  protected Formatter writerFormatter;
  protected HttpParser writerHttpParser;
  protected int writerIdxContentLengthHeader;

  public HttpTunnelClient(IOStreamFactory ioStreamFactory, String uri) throws IOException {
    this.ioStreamFactory = ioStreamFactory;
    this.writerHeadBuffer = new ByteArrayOutputStream(128);
    this.writerTailBuffer = new ByteArrayOutputStream(8);
    this.uri = uri;
    this.writerIdxContentLengthHeader = -1;
    openResponse(ioStreamFactory);
    initWriterBuffers();
  }

  public void close() throws IOException {
    try {
      closeRequest();
    } finally {
      this.ioResponse.close();
    }
  }

  public void requestWrite(byte[] b, int off, int len) throws IOException {
    openRequest(this.ioStreamFactory);
    try {
      Utils.writeTo(this.writerHeadBuffer, this.outRequest);
      this.writerFormatter.formatInt(len);
      Utils.writeTo(this.writerTailBuffer, this.outRequest);
      this.outRequest.write(b, off, len);
      this.outRequest.flush();
      this.writerHttpParser.parseResponse();
      int statusCode = this.writerHttpParser.getStatusCode();
      if (statusCode != 204) {
        int contentLength = (int)this.writerHttpParser.getHeaderValueLong(this.writerIdxContentLengthHeader);
        byte[] buffer = new byte[contentLength];
        DataInputStream dis = new DataInputStream(this.inRequest);
        dis.readFully(buffer);
        throwError("server returned: " + statusCode + " expected: 204 content: " + new String(buffer));
      }
    } finally {
      closeRequest();
    }
  }

  public int requestRead(byte[] b, int off, int len) throws IOException {
    return this.inResponse.read(b, off, len);
  }

  public int requestAvailable() throws IOException {
    return this.inResponse.available();
  }

  protected void openResponse(IOStreamFactory ioStreamFactory) throws IOException {
    this.ioResponse = ioStreamFactory.open();
    this.inResponse = this.ioResponse.getInput();
    this.outResponse = this.ioResponse.getOutput();
    initReader();
  }

  protected void openRequest(IOStreamFactory ioStreamFactory) throws IOException {
    this.ioRequest = ioStreamFactory.open();
    this.inRequest = this.ioRequest.getInput();
    this.outRequest = this.ioRequest.getOutput();
    this.writerHttpParser = new HttpParser(new Parser(this.inRequest));
    this.writerFormatter = new Formatter(this.outRequest);
    this.writerIdxContentLengthHeader = this.writerHttpParser.registerHeaderName("Content-Length");
  }

  protected void closeRequest() {
    try {
      this.ioRequest.close();
    } catch (Exception e) {
    }
  }

  protected void initReader() throws IOException {
    ByteArrayOutputStream readerBuffer = new ByteArrayOutputStream(128);
    Formatter formatter = new Formatter(readerBuffer);
    HttpFormatter httpFormatter = new HttpFormatter(formatter);
    httpFormatter.formatRequestLine("POST", this.uri, 1, 0);
    httpFormatter.formatHeaderLine("Content-Length", 0L);
    formatter.formatCRLF();
    Utils.writeTo(readerBuffer, this.outResponse);
    this.outResponse.flush();
    HttpParser httpParser = new HttpParser(new Parser(this.inResponse));
    int idxSessionHeader = httpParser.registerHeaderName(SESSION_HEADER_NAME);
    httpParser.parseResponse();
    int statusCode = httpParser.getStatusCode();
    if (statusCode != 200)
      throwError("server returned " + statusCode);
    this.sessionId = httpParser.getHeaderValue(idxSessionHeader);
    if (this.sessionId == null)
      throwError("header required: " + SESSION_HEADER_NAME);
  }

  protected void initWriterBuffers() throws IOException {
    this.writerHeadBuffer.reset();
    Formatter formatter = new Formatter(this.writerHeadBuffer);
    HttpFormatter httpFormatter = new HttpFormatter(formatter);
    httpFormatter.formatRequestLine("POST", this.uri, 1, 0);
    httpFormatter.formatHeaderConnectionKeepAlive();
    httpFormatter.formatHeaderLine(SESSION_HEADER_NAME, this.sessionId);
    httpFormatter.formatHeaderName("Content-Length");
    this.writerTailBuffer.reset();
    formatter = new Formatter(this.writerTailBuffer);
    formatter.formatCRLF();
    formatter.formatCRLF();
  }

  protected void throwError(String msg) throws IOException {
    try {
      close();
    } catch (Exception e) {
    }
    throw new IOException("HTTP tunnel client error: " + msg);
  }
}
